/*******************************************************************************
 * Copyright (c) 2019, 2023 fortiss GmbH, Primetals Technologies Austria GmbH
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *    Jose Cabral - initial implementation
 *    Martin Melik Merkumians - Change CIEC_STRING to std::string
 *******************************************************************************/

#ifndef SRC_MODULES_OPC_UA_OPCUA_CLIENT_INFORMATION_H_
#define SRC_MODULES_OPC_UA_OPCUA_CLIENT_INFORMATION_H_

#include "opcua_action_info.h"

#include "forte/arch/forte_sync.h"

#include <string>
#include <vector>

namespace forte::com_infra::opc_ua {
  /**
   * Contains all the information needed for a client to execute remote calls. It's the only class tha access
   * the OPC UA library for remote calls
   */
  class CUA_ClientInformation {
    public:
      /**
       * Constructor of the class
       * @param paEndpoint Endpoint of the remote
       */
      explicit CUA_ClientInformation(const std::string &paEndpoint);

      /**
       * Destructor of the class
       */
      ~CUA_ClientInformation();

      /**
       * Initialize the client
       * @return True if no error occurred, false otherwise
       */
      bool configureClient();

      /**
       * Removes all allocated information and sets the client to its default state
       */
      void uninitializeClient();

      /**
       * Try to connect to the remote endpoint and initialized all actions assigned
       * @return True if all actions were initialized, false otherwise
       */
      bool handleClientState();

      /**
       * Call the OPC UA API to execute all the asyncrhonous calls
       * @return True if no error ocurred, false otherwise
       */
      bool executeAsyncCalls();

      /**
       * Check if the client needs another async call. If a subscription action is present, this will stay true
       * @return True if a new async call is needed, false otherwise
       */
      inline bool isAsyncNeeded() const {
        return (0 != mMissingAsyncCalls);
      }

      /**
       * Check if an action is present in the client
       * @return True if an action is present in the client, false otherwise
       */
      bool hasActions() const {
        return !mActionsReferencingIt.empty();
      }

      /**
       * Check if at least one action was initialized in the last iteration of handleClientState()
       * @return True if at least one action was initialized in the last iteration of handleClientState(), false
       * otherwise
       */
      bool someActionWasInitialized() const {
        return mSomeActionWasInitialized;
      }

      /**
       * Getter of the endpoint
       * @return Endpoint of the clinet
       */
      const std::string &getEndpoint() const {
        return mEndpointUrl;
      }

      /**
       * Getter of the mutex of the client
       * @return Mutex of the clinet
       */
      inline arch::CSyncObject &getMutex() {
        return mClientMutex;
      }

      void setClientToInvalid() {
        mIsClientValid = false;
      }

      bool isClientValid() const {
        return mIsClientValid;
      }

      /**
       * Place the request to the OPC UA API for an asynchronous read of a remote variable
       * @param paActionInfo Action to be performed
       * @return UA_STATUSCODE_GOOD is no problem occurred, other value otherwise
       */
      UA_StatusCode executeRead(CActionInfo &paActionInfo);

      /**
       * Place the request to the OPC UA API for an asynchronous write of a remote variable
       * @param paActionInfo Action to be performed
       * @return UA_STATUSCODE_GOOD is no problem occurred, other value otherwise
       */
      UA_StatusCode executeWrite(CActionInfo &paActionInfo);

      /**
       * Place the request to the OPC UA API for an asynchronous method call of a remote variable
       * @param paActionInfo Action to be performed
       * @return UA_STATUSCODE_GOOD is no problem occurred, other value otherwise
       */
      UA_StatusCode executeCallMethod(CActionInfo &paActionInfo);

      /**
       * Add an action to the client
       * @param paActionInfo Action to be added
       */
      void addAction(CActionInfo &paActionInfo);

      /**
       * Remove an action from the client
       * @param paActionInfo Action to be removed
       */
      void removeAction(CActionInfo &paActionInfo);

      /**
       * Check if an action was already initialized in the client
       * @param paActionInfo Action to be checked
       * @return True if the action was already initialized, false otherwise
       */
      bool isActionInitialized(const CActionInfo &paActionInfo);

      /**
       * Class containing all remote callback functions that are called by the OPC UA stack
       */
      class CUA_RemoteCallbackFunctions {
        public:
          /**
           * Async callback for read action
           */
          static void
          readAsyncCallback(UA_Client *paClient, void *paUserdata, UA_UInt32 paRequestId, UA_ReadResponse *paResponse);

          /**
           * Async callback for write action
           */
          static void writeAsyncCallback(UA_Client *paClient,
                                         void *paUserdata,
                                         UA_UInt32 paRequestId,
                                         UA_WriteResponse *paResponse);

          /**
           * Async callback for method call action
           */
          static void
          callMethodAsyncCallback(UA_Client *paClient, void *paUserdata, UA_UInt32 paRequestId, void *paResponse);

          /**
           * Async callback for subscription action
           */
          static void subscriptionValueChangedCallback(UA_Client *paClient,
                                                       UA_UInt32 paSubId,
                                                       void *paSubContext,
                                                       UA_UInt32 paMonId,
                                                       void *paMonContext,
                                                       UA_DataValue *paValue);

          /**
           * Callback when the subscription (one for all monitored items) is deleted
           */
          static void
          deleteSubscriptionCallback(UA_Client *paClient, UA_UInt32 paSubscriptionId, void *paSubscriptionContext);

          /**
           * Function called when the state of the client changed
           */
          static void clientStateChangeCallback(UA_Client *paClient,
                                                UA_SecureChannelState channelState,
                                                UA_SessionState sessionState,
                                                UA_StatusCode connectStatus);
      };

    private:
      /**
       *  this is the same structure as UA_VariableContext_Handle in localhandler, but couldn't find where to put
       * without copying since there's always some include issue
       */
      struct UA_SubscribeContext_Handle {
          UA_SubscribeContext_Handle(CActionInfo &paActionInfo, size_t paPortIndex) :
              mActionInfo(&paActionInfo),
              mPortIndex(paPortIndex) {
          }

          // default copy constructor should be enough

          bool operator==(UA_SubscribeContext_Handle const &paRightObject) const {
            return (mActionInfo == paRightObject.mActionInfo && mPortIndex == paRightObject.mPortIndex);
          }

          CActionInfo *mActionInfo;
          size_t mPortIndex;
      };

      /**
       * Encapsulation of the information needed for a monitoring item (a variable subsription)
       */
      struct UA_MonitoringItemInfo {
          UA_MonitoringItemInfo(const UA_SubscribeContext_Handle &paVariableInfo, UA_UInt32 paMonitoringItemId = 0) :
              mVariableInfo(paVariableInfo),
              mMonitoringItemId(paMonitoringItemId) {
          }

          // default copy constructor should be enough

          bool operator==(UA_MonitoringItemInfo const &paRightObject) const {
            return (mVariableInfo == paRightObject.mVariableInfo &&
                    mMonitoringItemId == paRightObject.mMonitoringItemId);
          }

          UA_SubscribeContext_Handle mVariableInfo;
          UA_UInt32 mMonitoringItemId;
      };

      /**
       * Encapsulation of the information needed the subscription. In a subscription you can have many monitored items.
       * We have only one sunscription, and put all monitored items in it
       */
      struct UA_subscriptionInfo {
          UA_subscriptionInfo() : mSubscriptionId(0) {
          }

          UA_UInt32 mSubscriptionId;
          std::vector<std::unique_ptr<UA_MonitoringItemInfo>> mMonitoredItems;
      };

      /**
       * For read, write and call method, this encpasulation is used as a context to know who executed the action
       */
      class UA_RemoteCallHandle {
        public:
          UA_RemoteCallHandle(CActionInfo &paActionInfo, CUA_ClientInformation &paClientInformation) :
              mActionInfo(paActionInfo),
              mClientInformation(paClientInformation) {
          }
          ~UA_RemoteCallHandle() = default;

          CActionInfo &mActionInfo;
          CUA_ClientInformation &mClientInformation;

          UA_RemoteCallHandle(const UA_RemoteCallHandle &paObj) = delete;
          UA_RemoteCallHandle &operator=(const UA_RemoteCallHandle &other) = delete;
      };

      /**
       * Look for the configuration file and load the configuration from it, otherwise it loads the default
       * configuration
       * @param paConfigPointer Place to store the configuration
       * @return True if no error ocurred, false otherwise
       */
      bool configureClientFromFile(UA_ClientConfig &paConfigPointer);

      /**
       * Try to connect the client
       * @return True if the connection succeeded, false otherwise
       */
      bool connectClient();

      /**
       * Initialize all actions in the client.
       * @return True if all actions were initialized, false otherwise
       */
      bool initializeAllActions();

      /**
       * Initialize an action. It looks for the remote node and prepares for future calls
       * @param paActionInfo Action to be initialized
       * @return True if no error occurred, false otherwise
       */
      bool initializeAction(CActionInfo &paActionInfo);

      /**
       * A method call method needs special care. This function takes care of it
       * @param paActionInfo Method call action to be initialized
       * @return True if no error occurred, false otherwise
       */
      bool initializeCallMethod(CActionInfo &paActionInfo);

      /**
       * A subscription needs special care. This function takes care of it
       * @param paActionInfo Subscribe action to be initialized
       * @return True if no error occurred, false otherwise
       */
      bool initializeSubscription(CActionInfo &paActionInfo);

      /**
       * Create the subscription in the OPC UA stack
       * @return True if no error occurred, false otherwise
       */
      bool createSubscription();

      /**
       * Add a monitoring item to the OPC UA stack
       * @param paMonitoringInfo Contains the needed information to create the monitoring item in the OPC UA Stack
       * @param paNodeId NodeId of the variable to monitor
       * @return True if no error occurred, false otherwise
       */
      bool addMonitoringItem(UA_MonitoringItemInfo &paMonitoringInfo, const UA_NodeId &paNodeId);

      /**
       * Increments the amount of missing async calls to be performed. A subscription keeps always the missing calls at
       * least at
       */
      void addAsyncCall();

      /**
       * Removes the amount of missing async calls to be performed
       */
      void removeAsyncCall();

      /**
       * Uninitialze an action
       * @param paActionInfo Action to be uninitialized
       */
      void uninitializeAction(CActionInfo &paActionInfo);

      /**
       * Uninitialze a subscription action
       * @param paActionInfo Action to be uninitialized
       */
      void uninitializeSubscribeAction(const CActionInfo &paActionInfo);

      /**
       * Reset the subscription information.
       * @param paDeleteSubscription The subscription must be deleted from the OPC UA stack
       */
      void resetSubscription(bool paDeleteSubscription);

    public:
      CUA_ClientInformation(const CUA_ClientInformation &paObj) = delete;
      CUA_ClientInformation &operator=(const CUA_ClientInformation &other) = delete;

    private:
      /**
       * Endpoint of the remote
       */
      std::string mEndpointUrl;

      /**
       * Username to be used to connect to the server
       */
      std::string mUsername;

      /**
       * * Password to be used to connect to the server
       */
      std::string mPassword;

      /**
       * Handler of the OPC UA stack client
       */
      UA_Client *mClient{nullptr};

      /**
       * Mutex of the client
       */
      arch::CSyncObject mClientMutex;

      /**
       * Information needed for subscription
       */
      UA_subscriptionInfo mSubscriptionInfo;

      /**
       * Amount of missing async calls to be performed. Read, write and method call add one when they are called, and
       * reduced in the callbacks Subscription increases one when created, and only reduced when no more subscription
       * are present
       */
      size_t mMissingAsyncCalls{0};

      /**
       * List of actions that use this client
       */
      std::vector<CActionInfo *> mActionsReferencingIt;

      /**
       * List of actions that need to be initialized
       */
      std::vector<CActionInfo *> mActionsToBeInitialized;

      /**
       * Indicates if the client should wait scmConnectionRetryTimeoutNano before trying to reconnect. This is true when
       * an action fails to connect once
       */
      bool mNeedsReconnection{false};

      /**
       * Indicates if the client should wait scmInitializeActionRetryNano before trying to initialize the actions. This
       * is true when an action fails, so it doesn't fail too often. If an action is added after another fail, this
       * becomes false, so the new action can be initialized and doesn't have to wait
       */
      bool mWaitToInitializeActions{false};

      /**
       * Indicate the client is about to be deleted, so it's not added to new lists.
       * The reason behind this variable, is because the following race condition happened in
       * COPC_UA_Remote_Handler::removeClientFromAllLists
       *  - The client is removed from the connection list
       *  - Before removing from the normal handler list, the async call fails and the client is reconfigured and
       * re-added to the connection handler
       *  - The Client is deleted from allClients lists
       *  - The Client is deleted (C++ wise)
       *  - Since the connectionHandler has still the client, it tries to use it and crash
       *
       *  So this variable prevents the re-adding to any iteration list if it was set already to invalid, when is about
       * to be deleted
       */
      bool mIsClientValid{true};

      /**
       * Store the time when the connection last failed
       */
      uint_fast64_t mLastReconnectionTry{0};

      /**
       * Store the time when an action last failed to initialized
       */
      uint_fast64_t mLastActionInitializationTry{0};

      /**
       * True if an action was initialized in the last iteration of handleClientState()
       */
      bool mSomeActionWasInitialized{false};

      /**
       * Time in nanoseconds that the client should wait before another reconnection try
       */
      static const uint_fast64_t scmConnectionRetryTimeoutNano = static_cast<uint_fast64_t>(8E9); // 8s

      /**
       * Time in nanoseconds that the client should wait before try to initialize the actions
       */
      static const uint_fast64_t scmInitializeActionRetryNano = static_cast<uint_fast64_t>(3E9); // 3s

      /**
       * Time in milliseconds for connection timeout configured in the OPC UA stack client
       */
      static const UA_UInt32 scmClientTimeoutInMilli = static_cast<UA_UInt32>(5E3); // 5s
  };
} // namespace forte::com_infra::opc_ua
#endif /* SRC_MODULES_OPC_UA_OPCUA_CLIENT_INFORMATION_H_ */
